/*
 * Copyright 2008-2023 dexian.vip. All rights reserved.
 * Support: http://www.dexian.vip
 * License: http://www.dexian.vip/license
 */

package vip.dexian.admin.security.config;

import vip.dexian.admin.security.config.custom.CustomDaoAuthenticationProvider;
import vip.dexian.admin.security.dynamic.AuthorityMapper;
import vip.dexian.admin.security.strategy.ExpiredSessionStrategy;
import vip.dexian.password.encoder.PasswordEncoder;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;

/**
 * Spring security 配置
 *
 * @author 挺好的 2023年06月05日 15:28
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    /**
     * 登录页面
     */
    public static final String LOGIN = "/login";

    /**
     * 忽略的请求
     */
    private static final String[] IGNORE_REQUESTS = new String[] {"/resources/**", "/error/**", "/captcha", "/favicon.ico", "/robots.txt",};

    /**
     * 密码加密器
     */
    @Resource (name = "passwordEncoder")
    private PasswordEncoder passwordEncoder;

    /**
     * 管理员
     */
    @Resource (name = "adminManagerImpl")
    private UserDetailsService adminService;

    /**
     * 认证失败
     */
    @Resource (name = "simpleAuthenticationFailureHandler")
    private AuthenticationFailureHandler authenticationFailureHandler;

    /**
     * 认证成功
     */
    @Resource (name = "simpleAuthenticationSuccessHandler")
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    /**
     * 权限不足
     */
    @Resource (name = "simpleAccessDeniedHandler")
    private AccessDeniedHandler simpleAccessDeniedHandler;

    /**
     * 认证失败错误处理
     */
    @Resource (name = "simpleAuthenticationEntryPoint")
    private AuthenticationEntryPoint simpleAuthenticationEntryPoint;

    /**
     * 是否是调试模式
     */
    @Value ("${is-debug}")
    private Boolean isDebug;

    /**
     * session 过期策略
     */
    @Resource (name = "expiredSessionStrategy")
    private ExpiredSessionStrategy expiredSessionStrategy;

    /**
     * 注册SessionRegistry
     */
    @Bean
    SessionRegistry sessionRegistry () {
        return new SessionRegistryImpl();
    }

    /**
     * 认证处理
     */
    @Bean
    public AuthenticationProvider authenticationProvider () {
        // 使用DaoAuthenticationProvider， 此处定义的将会覆盖原有的
        CustomDaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider();

        authenticationProvider.setIsDebug(this.isDebug);
        // 设置UserDetailsService
        authenticationProvider.setUserDetailsService(this.adminService);
        // 权限映射器
        authenticationProvider.setAuthoritiesMapper(new AuthorityMapper());
        // 密码加密
        authenticationProvider.setPasswordEncoder(this.passwordEncoder.getPasswordEncoder());

        return authenticationProvider;
    }

    /**
     * 使用HttpStatusRequestRejectedHandler，替换默认的DefaultRequestRejectedHandler。
     * <p>
     * DefaultRequestRejectedHandler 默认是直接抛出异常，无法在Controller的全局异常处理中捕获，状态码直接为500，这样对于用户而言不是很友好
     * <p>
     * HttpStatusRequestRejectedHandler设置状态码为400（可以自定义自己想要的状态码），BAD_REQUEST
     *
     * @return {@link org.springframework.security.web.firewall.RequestRejectedHandler}
     */
    @Bean
    WebSecurityCustomizer webSecurityCustomizer () {
        // ignore的配置并不推荐在此，但是，对于动态权限而言，如果不在此配置忽略，那么所有的资源都将进入动态资源过滤器
        return web -> web.requestRejectedHandler(new HttpStatusRequestRejectedHandler()).ignoring()
                .requestMatchers(IGNORE_REQUESTS);
    }

    @Bean
    public SecurityFilterChain filterChain (HttpSecurity http,
            AuthorizationManager <RequestAuthorizationContext> authorizationManager) throws Exception {
        // 配置iframe 同源可以使用
        http.headers(headerConfig -> headerConfig.frameOptions((HeadersConfigurer.FrameOptionsConfig::sameOrigin)));


        return http.authorizeHttpRequests(authorizeRequests -> authorizeRequests

                        .requestMatchers(LOGIN).permitAll()

                        .anyRequest()

                        // 使用自定义的manager
                        .access(authorizationManager)

                )
                // 登录
                .formLogin(formLogin -> formLogin.loginPage(LOGIN).loginProcessingUrl(LOGIN)
                        .successHandler(this.authenticationSuccessHandler)
                        .failureHandler(this.authenticationFailureHandler))

                // 退出
                .logout(logout -> logout.logoutSuccessUrl(LOGIN).clearAuthentication(true).invalidateHttpSession(true)
                        .deleteCookies("SESSION", "XSRF-TOKEN", "JSESSIONID"))

                // 认证实现
                .authenticationProvider(this.authenticationProvider())

                // 认证失败
                .exceptionHandling(
                        exceptionHandling -> exceptionHandling.accessDeniedHandler(this.simpleAccessDeniedHandler)
                                .authenticationEntryPoint(this.simpleAuthenticationEntryPoint))

                // session 管理
                .sessionManagement(sessionManagement -> sessionManagement.invalidSessionUrl(LOGIN).maximumSessions(1)
                        .expiredUrl(LOGIN).expiredSessionStrategy(this.expiredSessionStrategy)
                        .sessionRegistry(this.sessionRegistry()))

                // csrf 防御
                .csrf(httpSecurityCsrfConfigurer -> {

                    CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();

                    // 禁用CsrfToken的BREACH保护，恢复到5.8版本的默认值。对于有BREACH保护的csrfToken，设置值到cookie的值和生成的csrfToken
                    // 两个值是不一致的，所以导致csrf不通过。目前还未弄清除是什么时机设置到cookie中的或者是否有其他方式修改cookie的值。
                    // 另外的实现： XorCsrfTokenRequestAttributeHandler
                    httpSecurityCsrfConfigurer.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler());

                    httpSecurityCsrfConfigurer.csrfTokenRepository(csrfTokenRepository);
                })

                .build();


    }
}
